#  icmp.rb
#
#  This file is part of myvnet.
#
#  Copyright (C) 2008  Filipe David Borba Manana,  fdmanana@gmail.com
#
#  myvnet is free software: you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or
#  (at your option) any later version.
#
#  myvnet is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with myvnet.  If not, see <http://www.gnu.org/licenses/>.


require 'lib/protocols/generic_protocol'
require 'lib/protocols/ipv4'
require 'lib/exceptions'


# Internet Control Message Protocol (ICMP) for IPv4 module.
module ICMP
  
  # maps ICMP packet type codes to symbols
  TYPE_SYM = {
    0x00 => :echo_reply,
    0x03 => :dst_unreachable,
    0x04 => :src_quench,
    0x05 => :redirect_msg,
    0x08 => :echo_request,
    0x0b => :time_exceeded,
    0x0c => :bad_ip_header,
    0x0f => :info_request,
    0x10 => :info_reply,
    0x1e => :traceroute,
  }.freeze
  
  # maps ICMP packet type symbols to codes
  TYPE_CODE = TYPE_SYM.invert.freeze
  
  
  # ICMP packet header length (in bytes)
  HLEN = 8
  
  
  # ICMP packet.
  #
  class Packet < GenericProtocol::Packet
    
    @packet_type = :icmp
    @layer = 3
    
    attr_accessor :type      # ICMP packet type
    attr_accessor :code
    attr_accessor :checksum
    attr_accessor :id
    attr_accessor :seq       # sequence
    
    attr_accessor :pointer   # overlaps with most significant byte of the ID field - used by 
                             # packets of type "Parameter Problem Message" (:bad_ip_header)
                             
    attr_accessor :gwaddr    # Gateway Internet Address - overlaps with ID + SEQUENCE fields -
                             # used by packets of type "Redirect Message" (:redirect_msg).
                             # Field value should be a String containing an IPv4 address in
                             # the dotted notation.
    
    def initialize(bstream = nil)
      super
      
      if bstream.nil?
        bstream = ("\x00" * HLEN)
      end
      
      @type = (bstream[0,1].unpack 'C')[0]
      @type = TYPE_SYM[@type] || @type
      
      @code = (bstream[1,1].unpack 'C')[0]
      @checksum = (bstream[2,2].unpack 'n')[0]
      @payload = bstream[8..-1]
      
      case @type
        when :bad_ip_header
          @pointer = (bstream[4,1].unpack 'C')[0]
        when :redirect_msg
          @gwaddr = IPv4.to_friendly_addr(bstream[4,4])
        else
          @id = (bstream[4,2].unpack 'n')[0]
          @seq = (bstream[6,2].unpack 'n')[0]
      end

    end


    def initialize_copy(orig)
      super
      @gwaddr = orig.gwaddr.dup if orig.gwaddr.is_a? String
    end
    

    # === Synopsis
    #
    #   ICMP::Packet#to_bstream
    #
    # === Return
    #
    # A byte stream, in network byte order, representation of the receiver
    # ICMP packet.
    #
    # === Description
    #
    # Builds a byte stream (String instance), in network byte order, 
    # representation of the receiver ICMP packet.
    #
    # *Note*: If the receiving packet's checksum is zero, the checksum is 
    # calculated and set.
    #    
    def to_bstream
      stream = ''
    
      if @type.is_a? Symbol
        stream << ( [ TYPE_CODE[@type] ].pack 'C' )
      else
        stream << ( [@type].pack 'C' )
      end
      
      stream << ( [@code].pack 'C' )
      stream << ( [@checksum].pack 'n' )
      
      case @type
        when :bad_ip_header
          stream << ( [@pointer].pack 'C' ) << ("\x00" * 3)
        when :redirect_msg
          stream << ( IPv4.to_bstream_addr(@gwaddr) )
        else
          stream << ( [@id].pack 'n' )
          stream << ( [@seq].pack 'n' )          
      end
      
      case @payload
        when String
          stream << @payload
        when IPv4::Packet
          stream << (@payload.to_bstream)
      end

      set_checksum stream if @checksum.zero?
      stream
    end
    
    
    # === Synopsis
    # 
    #   ICMP::Packet#checksum_calc!
    #   
    # === Return
    # 
    # The checksum that was computed for the ICMP packet.
    # 
    # === Description
    # 
    # Computes the checksum of an ICMP packet and sets the receiver
    # packet's checksum field value to the computed value.
    #    
    def checksum_calc!
      @checksum = 0
      self.to_bstream      
      @checksum
    end
    
    
    private
    
    def set_checksum(bstream)
      # if odd number of bytes, add an extra one
      padded = false
      if bstream.length[0] == 1
        bstream << "\x00"
        padded = true
      end
      
      bstream[2,2] = "\x00\x00"
      words = bstream.unpack 'n*'

      sum = words.inject do |w,a|
        w + a
      end
      
      sum = (sum & 0xff_ff) + (sum >> 16)
      chksum = (~sum) & 0xff_ff
      @checksum = chksum
      
      bstream[2,2] = [chksum].pack 'n'      
      bstream[-1,1] = '' if padded

      bstream
    end
  
  end
  
end